using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net.Mail;
using System.Net;
using System.Threading;
using System.Configuration;
using System.ServiceProcess;
using System.Collections.Specialized;
using System.Windows.Forms;

namespace GSyncConsole
{
    class Program
    {
        bool mailSent;
        List<FileStateData> fileStateList = new List<FileStateData>();
        
        [STAThread]
        static void Main(string[] args)
        {
            try
            {
                Program p = new Program();

                if (args.Length > 0)
                {
                    if (args[0].Equals("Merge"))
                    {
                        p.MergeFiles();
                    }
                }
                else
                {
                    
                    p.Run();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception:" + e.ToString());
            }

            Console.WriteLine("Press any key to continue...");
            Console.ReadLine();
        }

        /// <summary>
        /// Merge chunks of files
        /// </summary>
        private void MergeFiles()
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Title = "Select first part of file...";

            DialogResult dr = openFileDialog.ShowDialog();

            if (dr == DialogResult.OK)
            {
                string filename = openFileDialog.FileName;

                MergeFile merge = new MergeFile(filename);
                merge.Start();
            }
        }

        private void Run()
        {
            if (Settings.CheckAndRunStunnelOnStartUp)
            {
                if (!Setup())
                {
                    Console.WriteLine("Unable to find Stunnel service. Please install stunnel before running GmailSync.");
                    return;
                }
            }

            this.LoadFileStateData();

            if (Directory.Exists(Settings.SourceDirectory))
            {                
                try
                {
                    SearchOption searchOption = SearchOption.TopDirectoryOnly;

                    if (Settings.ScanSubDirectories)
                    {
                        searchOption = SearchOption.AllDirectories;
                    }

                    string[] filesInDirectory = Directory.GetFiles(Settings.SourceDirectory, "*.*", searchOption);
                    
                    foreach (string file in filesInDirectory)
                    {
                        FileInfo fileInfo = new FileInfo(file);

                        if (IsFileRequiredForBackup(fileInfo))
                        {
                            FileSize fileSize = new FileSize(fileInfo.Length);
                            string filename = Path.GetFileName(fileInfo.FullName);
                            string fullPath = GetTemporaryFilename(fileInfo.FullName);

                            // rename the file to attach to email
                            if (!fileInfo.FullName.Equals(fullPath))
                            {
                                File.Move(fileInfo.FullName, fullPath);
                            }

                            Console.WriteLine("Filename:{0}, Size:{1}", filename, fileSize.NormalizeLength);

                            if (fileSize.SizeInMB < 10)
                            {
                                SendMail(fileInfo, filename, fullPath);
                                AddFileStateToList(fileInfo);
                            }
                            else
                            {
                                Console.WriteLine(">>>> File {0} size {1} is >= 9 MB. Trying to split file", fileInfo.FullName, fileSize.NormalizeLength);

                                StringCollection splitFilenames = SplitFile(fullPath, fileSize.FileLength);

                                foreach (string splitFilename in splitFilenames)
                                {
                                    Console.WriteLine("Filename:" + splitFilename);
                                    FileInfo splitFileInfo = new FileInfo(splitFilename);
                                    SendMail(splitFileInfo, splitFilename);
                                    AddFileStateToList(splitFileInfo);
                                }

                                Thread.Sleep(2000);

                                // clean temporary file chunks
                                CleanTemporaryFiles(splitFilenames);
                            }
                        }
                    }

                    // wait for 2 seconds so that the process releases the handle on the files
                    Thread.Sleep(2000);

                    // now move all the files to the target directory
                    foreach (string file in filesInDirectory)
                    {
                        FileInfo fileInfo = new FileInfo(file);                        
                        MoveFileToDestinationDirectory(fileInfo);
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine(">>>> Exception:" + e.ToString());
                }
            }
            else
            {
                Console.WriteLine("Source Directory does not exist");
            }

            this.RemoveOldEntries();
            this.SaveFileStateData();
        }

        /// <summary>
        /// Clean files generated during split
        /// </summary>
        /// <param name="splitFilenames"></param>
        private void CleanTemporaryFiles(StringCollection splitFilenames)
        {
            foreach (string splitFilename in splitFilenames)
            {
                if (File.Exists(splitFilename))
                {
                    File.Delete(splitFilename);
                }
            }
        }

        private void SendMail(FileInfo splitFileInfo, string filename)
        {
            SendMail(splitFileInfo, Path.GetFileNameWithoutExtension(filename), filename);            
        }

        /// <summary>
        /// Split a file into chunks. 
        /// </summary>
        /// <param name="fullPath"></param>
        /// <param name="originalFilesize"></param>
        /// <returns></returns>
        private StringCollection SplitFile(string fullPath, long originalFilesize)
        {
            StringCollection splitFilenames = new StringCollection();

            int splitFileSizeInBytes = Settings.SplitFileSize * 1024 * 1024;
                        
            // check if any file part is remaining
            long remainingBytes = originalFilesize % splitFileSizeInBytes;

            int totalParts = (int)originalFilesize / (splitFileSizeInBytes) + ((remainingBytes != 0) ? 1 : 0);

            // prepare the file to read
            FileStream partStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            BinaryReader partReader = new BinaryReader(partStream);

            byte[] readbuffer = new byte[splitFileSizeInBytes];
                
            for (int i = 1; i <= totalParts; i++)
            {
                string filename = GenerateFile(i, totalParts, fullPath);
                splitFilenames.Add(filename);

                readbuffer = partReader.ReadBytes(splitFileSizeInBytes);

                if (!File.Exists(filename))
                {
                    // generate file
                    File.WriteAllBytes(filename, readbuffer);
                }
                else
                {
                    Console.WriteLine("File: {0} already exists", filename);
                }
            }               

            partReader.Close();
            partStream.Close();

            return splitFilenames;

        }

        /// <summary>
        /// Generate filename for a chunk during split
        /// </summary>
        /// <param name="fileIndex"></param>
        /// <param name="totalParts"></param>
        /// <param name="fullPath"></param>
        /// <returns></returns>
        private string GenerateFile(int fileIndex, int totalParts, string fullPath)
        {
            return string.Format("{0}.(Part{1}-{2}){3}",
                    Path.Combine(Path.GetDirectoryName(fullPath), Path.GetFileNameWithoutExtension(fullPath)),
                    fileIndex,
                    totalParts,
                    Path.GetExtension(fullPath)
                    );
        }

        /// <summary>
        /// Checks if STunnel service is installed and running
        /// </summary>
        private bool Setup()
        {
            ServiceController[] services = ServiceController.GetServices();
            
            bool foundService = false;

            foreach (ServiceController service in services)
            {
                if (String.Compare(service.ServiceName, Settings.StunnelServiceName) == 0)
                {
                    foundService = true;

                    if (service.Status != ServiceControllerStatus.Running)
                    {
                        Console.WriteLine("Stunnel service not running. Trying to start the service. Wait for 5 seconds");
                        service.Start();

                        Thread.Sleep(5000);
                    }                        
                }
            }

            return foundService;
        }

        /// <summary>
        /// Remove the entries from the FileState list if the files are not present in the
        /// directory
        /// </summary>
        private void RemoveOldEntries()
        {
            foreach (FileStateData fileState in fileStateList)
            {
                if (!File.Exists(fileState.Filename))
                {
                    fileStateList.Remove(fileState);
                    break;
                }
            }
        }

        /// <summary>
        /// Add the file state data structure to the list. If the file is already present,
        /// then just update the fields
        /// </summary>
        /// <param name="fileInfo"></param>
        private void AddFileStateToList(FileInfo fileInfo)
        {
            // first check if the file is already in the list
            int index = LookupFileInList(fileInfo.FullName);

            if (index == -1)
            {
                fileStateList.Add(new FileStateData(fileInfo));
            }
            else
            {
                fileStateList[index].LastModificationDate = fileInfo.LastWriteTime;
                fileStateList[index].BackedUp = true;
                fileStateList[index].LastSync = DateTime.Now;
            }
            
        }

        /// <summary>
        /// Checks whether the file is to be included in the backup list.
        /// First check if the file exists in the list. If it is, then check if the
        /// last modification date is changed and include it in the backup list. If 
        /// it is not in the old list then obviously it is a candidate for backup
        /// </summary>
        /// <param name="fileInfo"></param>
        /// <returns></returns>
        private bool IsFileRequiredForBackup(FileInfo fileInfo)
        {
            bool fileRequired = false;

            int index = LookupFileInList(fileInfo.FullName);

            if (index == -1)
            {
                fileRequired = true;
            }
            else
            {                
                FileStateData fileState = this.fileStateList[index] as FileStateData;

                if (DateTime.Compare(fileState.LastModificationDate, fileInfo.LastWriteTime) != 0)
                {
                    fileRequired = true;
                }
            }

            return fileRequired;            
        }

        /// <summary>
        /// Searches for the file in the list
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        private int LookupFileInList(string filename)
        {
            int index = -1;

            for (int currentIndex = 0; currentIndex < this.fileStateList.Count; currentIndex++)
            {
                FileStateData fileState = this.fileStateList[currentIndex] as FileStateData;

                if (String.Compare(fileState.Filename, filename) == 0)
                {
                    index = currentIndex;
                    break;
                }
            }

            return index;
        }

        /// <summary>
        /// Move the file in the destination directory. It depends on whether the 
        /// 'DestinationDirectory' configuration item is populated.
        /// </summary>
        /// <param name="fileInfo"></param>
        private void MoveFileToDestinationDirectory(FileInfo fileInfo)
        {
            string filename = Path.GetFileName(fileInfo.FullName);

            // move the file to the target directory
            if (!String.IsNullOrEmpty(Settings.DestinationDirectory))
            {
                string targetFilename = Path.Combine(Settings.DestinationDirectory, filename);

                if (File.Exists(targetFilename))
                {
                    Console.WriteLine(">>>> File already exists in the destination folder");
                }
                else
                {
                    File.Move(GetTemporaryFilename(fileInfo.FullName), targetFilename);
                }
            }
            else
            {
                // check if file is in temporary filename
                if (File.Exists(GetTemporaryFilename(fileInfo.FullName)))
                {
                    // otherwise, only rename the file back to the original name
                    File.Move(GetTemporaryFilename(fileInfo.FullName), fileInfo.FullName);
                }
                
            }
        }

        /// <summary>
        /// Sends the mail to gmail through STunnel
        /// </summary>
        /// <param name="fileInfo"></param>
        /// <param name="filename"></param>
        /// <param name="fullPath"></param>
        private void SendMail(FileInfo fileInfo, string filename, string fullPath)
        {
            if (Settings.FullDirectoryPathInSubject)
            {
                filename = fullPath;
            }

            mailSent = false;

            MailAddress toAddress = new MailAddress(Settings.EmailTo);
            MailAddress fromAddress = new MailAddress(Settings.EmailFrom);

            MailMessage mail = new MailMessage(fromAddress, toAddress);
            mail.Subject = string.Format("{0} {1}", Settings.EmailFilter, filename); ;
            mail.Body = filename;
            Attachment attachment = new Attachment(fullPath);
            mail.Attachments.Add(attachment);

            SmtpClient smtpClient = new SmtpClient(Settings.SmtpServer, Settings.SmtpPort);
            smtpClient.SendCompleted += new SendCompletedEventHandler(smtpClient_SendCompleted);
            //smtpClient.EnableSsl = true;
            smtpClient.Credentials = new NetworkCredential(Settings.SmtpUser, Settings.SmtpPass);
            smtpClient.SendAsync(mail, fileInfo);

            while (!mailSent)
            {
                Thread.Sleep(500);
            }

            mail.Dispose();
        }

        /// <summary>
        /// Serialize the sync data in a file
        /// </summary>
        private void SaveFileStateData()
        {
            // put in try catch, show exceptions
            Settings.SaveData(this.fileStateList);
        }

        /// <summary>
        /// Deserializes the sync data from the file
        /// </summary>
        private void LoadFileStateData()
        {
            // put in try catch, show exceptions
            this.fileStateList = Settings.LoadData();
        }

        /// <summary>
        /// Checks if the file extension is invalid according to the configuration item
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        private bool IsInvalidExtension(string filename)
        {
            bool isInvalidExtension = false;
            string inValidExtensions = Settings.InvalidExtention;
            string ext = Path.GetExtension(filename);

            int index = inValidExtensions.IndexOf(ext);
            if (index >= 0)
            {
                isInvalidExtension = true;
            }

            return isInvalidExtension;
        }

        /// <summary>
        /// Gets the temporary file name for the files with invalid extensions
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        public string GetTemporaryFilename(string filename)
        {
            string tempFilename = filename;

            if (IsInvalidExtension(filename))
            {
                tempFilename = string.Format("{0}{1}", filename, Settings.ValidExtension);
            }

            return tempFilename;
        }

        /// <summary>
        /// This is called once the email is sent successfully
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void smtpClient_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
        {
            // Get the unique identifier for this asynchronous operation.
            FileInfo fileInfo = (FileInfo)e.UserState;

            string filename = Path.GetFileName(fileInfo.FullName);
            Console.WriteLine(">>>> File {0} sent", filename);
            
            if (e.Error != null)
            {
                Console.WriteLine("[{0}] {1}", fileInfo.FullName, e.Error.ToString());
            }
            
            mailSent = true;
        }
    }
}
